Source of truth (2026) — 20 files in Editor/UOW/
  • UnitofWork.Core.cs — Constructors (7), ~30 properties, OBL wiring, helper composition, ChangeSummary, Export/Import, Dispose
  • UnitofWork.CRUD.cs — Read/Get/New/Add/Update/Insert/Delete with async variants
  • UnitofWork.Core.Extensions.cs — 13 lifecycle events (Pre/Post Create,Insert,Update,Delete,Query,Commit,Edit), Commit/Rollback, AddRange/UpdateRange/DeleteRange, RevertItem, FindAsync
  • UnitofWork.Core.Utilities.cs — Clear, navigation (MoveFirst/Next/Previous/Last/MoveTo), state queries (DocExist, FindDocIdx, Getindex, GetTrackingItem)
  • UnitofWork.OBLIntegration.cs — Validation, Undo/Redo, Computed Columns, Bookmarks, Thread Safety, Aggregates, Virtual Loading
  • IUnitofWorkCapabilities.cs — 7 interfaces: IRevertable, IBatchCommittable, IExportable, IImportable, IAggregatable, IUndoable, IMergeable, IUnitofWorkHistory
  • UnitOfWorkFactory.cs (517 lines) — Runtime type creation, caching, validation
  • UnitOfWorkWrapper.cs (1164 lines) — Late-bound wrapper with full CRUD, navigation, events
  • UnitOfWorkWrapperExtensions.cs — ForEachRecordAsync, FindRecord, BatchInsertAsync, etc.
  • Helpers/ — StateHelper, DataHelper, CollectionHelper, ValidationHelper, EventHelper, DefaultsHelper, QueryHistory, ExportHelper
  • Models/ — QueryHistoryEntry, ChangeSummary, CommitBatchProgress, CommitBatchResult

Overview

The UnitOfWork<T> class implements the Unit of Work pattern for managing a collection of entities of type T. It provides change tracking, validation, CRUD operations, and transaction management for a single entity type within a data source.

🎯 Key Features
  • Generic implementation for any Entity type
  • Automatic change tracking and state management
  • Observable collections with property change notifications
  • Field-level change inspection via OBL (Phase 1)
  • Filtering and pagination support
  • Full Undo/Redo stack (IUndoable)
  • Batch commit with chunked progress (IBatchCommittable)
  • Per-item revert & server-merge (IRevertable / IMergeable)
  • JSON / CSV export and import (IExportable / IImportable)
  • Change history & query audit log (IUnitofWorkHistory)
  • Advanced in-memory aggregates: SumWhere, GroupBy, etc. (IAggregatable)
  • Async CRUD operations
  • Event-driven architecture with lifecycle hooks
  • Validation and error handling

Signature-Verified Snapshot

public partial class UnitofWork<T>
    : IUnitofWork<T>, INotifyPropertyChanged,
      IRevertable, IBatchCommittable, IExportable, IImportable,
      IAggregatable, IUndoable, IMergeable, IUnitofWorkHistory
    where T : Entity, new()
{
    public UnitofWork();
    public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName);
    public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName, EntityStructure entityStructure);
    public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName, string primarykey);
    public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName, EntityStructure entityStructure, string primarykey);
    public UnitofWork(IDMEEditor dMEEditor, bool isInListMode, ObservableBindingList<T> ts);
    public UnitofWork(IDMEEditor dMEEditor, bool isInListMode, ObservableBindingList<T> ts, string primarykey);

    public bool IsDirty { get; }
    public bool IsLogging { get; set; }
    public string SoftDeleteFieldName { get; set; }
    public bool IncludeDeleted { get; set; }
    public ConcurrencyMode ConcurrencyMode { get; set; }
    public CommitOrder CommitOrder { get; set; }
}

Constructors

Basic Constructor

public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName, string primarykey)

With Entity Structure

public UnitofWork(IDMEEditor dMEEditor, string datasourceName, string entityName, EntityStructure entityStructure, string primarykey)

List Mode Constructor

public UnitofWork(IDMEEditor dMEEditor, bool isInListMode, ObservableBindingList<T> ts, string primarykey)
Parameter Type Description
dMEEditor IDMEEditor Data management editor instance
datasourceName string Name of the data source
entityName string Name of the entity
primarykey string Name of the primary key field
entityStructure EntityStructure Metadata about the entity structure
isInListMode bool True for in-memory operations only
ts ObservableBindingList<T> Initial collection for list mode

Usage Examples

// Create UnitOfWork for Customer entities
var customerUoW = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID");

// Create UnitOfWork with entity structure
var orderUoW = new UnitofWork<Order>(dmeEditor, "MongoDB", "Orders", orderStructure, "OrderID");

// Create UnitOfWork in list mode (in-memory)
var tempCustomers = new ObservableBindingList<Customer>();
var listUoW = new UnitofWork<Customer>(dmeEditor, true, tempCustomers, "CustomerID");

Core Properties

Property Type Description
Units ObservableBindingList<T> The main collection of entities (filtered or unfiltered)
FilteredUnits ObservableBindingList<T> Filtered collection when filters are applied
CurrentItem T Currently selected entity in the collection
IsDirty bool True if there are uncommitted changes
IsInListMode bool True for in-memory operations only
PrimaryKey string Name of the primary key field
EntityName string Name of the entity
DataSource IDataSource Associated data source
IsIdentity bool True if primary key is auto-generated
IsLogging bool Enable/disable change logging
SoftDeleteFieldName string Field name used for soft-delete behavior
IncludeDeleted bool Includes soft-deleted rows in retrieval queries
ConcurrencyMode ConcurrencyMode Optimistic concurrency behavior for updates/deletes
CommitOrder CommitOrder Commit ordering strategy for FK-safe persistence

Data Retrieval Methods

Get All Entities

public virtual async Task<ObservableBindingList<T>> Get()

Retrieves all entities from the data source.

Get with Filters

public virtual async Task<ObservableBindingList<T>> Get(List<AppFilter> filters)

Retrieves entities with applied filters.

Custom Query

public virtual async Task<ObservableBindingList<T>> GetQuery(string query)

Executes a custom query and returns results.

Get by Primary Key

public virtual T Get(string primaryKeyId)

Gets a single entity by its primary key value.

Usage Examples

// Get all customers
var allCustomers = await customerUoW.Get();

// Get customers with filters
var filters = new List<AppFilter> 
{
    new AppFilter {FieldName = "Country", Operator = "=", FilterValue = "USA" },
    new AppFilter {FieldName = "Status", Operator = "=", FilterValue = "Active" }
};
var filteredCustomers = await customerUoW.Get(filters);

// Get customer by ID
var customer = customerUoW.Get("CUST001");

// Custom query
var queryResult = await customerUoW.GetQuery("SELECT * FROM Customers WHERE CreateDate > '2023-01-01'");

CRUD Operations

Create Operations

public void New()        // Creates a new entity and adds it to the collection
public void Add(T entity) // Adds an existing entity to the collection

Insert Operations

public async Task<IErrorsInfo> InsertAsync(T doc)  // Async insert to data source
public IErrorsInfo InsertDoc(T doc)               // Sync insert to data source

Update Operations

public async Task<IErrorsInfo> UpdateAsync(T doc)  // Async update in data source
public IErrorsInfo Update(T entity)               // Update entity in collection

Delete Operations

public async Task<IErrorsInfo> DeleteAsync(T doc)  // Async delete from data source
public IErrorsInfo Delete(T entity)               // Delete entity from collection
public IErrorsInfo Delete(string id)              // Delete entity by primary key

Complete CRUD Example

using (var customerUoW = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID"))
{
    // Create new customer
    customerUoW.New();
    var newCustomer = customerUoW.CurrentItem;
    newCustomer.Name = "John Doe";
    newCustomer.Email = "john@example.com";

    // Add existing customer
    var existingCustomer = new Customer { Name = "Jane Smith" };
    customerUoW.Add(existingCustomer);

    // Insert to database
    var insertResult = await customerUoW.InsertAsync(newCustomer);
    if (insertResult.Flag == Errors.Ok)
    {
        Console.WriteLine("Customer inserted successfully");
    }

    // Update customer
    newCustomer.Email = "newemail@example.com";
    var updateResult = await customerUoW.UpdateAsync(newCustomer);

    // Delete customer
    var deleteResult = await customerUoW.DeleteAsync(newCustomer);

    // Delete by ID
    customerUoW.Delete("CUST001");
}

Transaction Management

Commit Operations

public virtual async Task<IErrorsInfo> Commit()
public virtual async Task<IErrorsInfo> Commit(IProgress<PassedArgs> progress, CancellationToken token)

Commits all pending changes to the data source with optional progress reporting and cancellation support.

Rollback Operations

public Task<IErrorsInfo> Rollback()

Rolls back all uncommitted changes.

Transaction Example

try
{
    // Make multiple changes
    customerUoW.New();
    var customer1 = customerUoW.CurrentItem;
    customer1.Name = "Customer 1";
    
    customerUoW.New();
    var customer2 = customerUoW.CurrentItem;
    customer2.Name = "Customer 2";
    
    // Commit all changes
    var result = await customerUoW.Commit();
    
    if (result.Flag == Errors.Ok)
    {
        Console.WriteLine("All changes committed");
    }
    else
    {
        Console.WriteLine($"Commit failed: {result.Message}");
        await customerUoW.Rollback();
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    await customerUoW.Rollback();
}

Change Tracking

The UnitOfWork automatically tracks changes to entities with the following states:

➕ Added

New entities not yet persisted to the data source

✏️ Modified

Existing entities with property changes

🗑️ Deleted

Entities marked for removal from data source

➖ Unchanged

Entities with no modifications since last commit

Change Tracking Methods

public IEnumerable<int> GetAddedEntities()      // Get indexes of added entities
public IEnumerable<int> GetModifiedEntities()   // Get indexes of modified entities
public IEnumerable<T> GetDeletedEntities()      // Get deleted entities
public bool GetIsDirty()                        // Check if there are uncommitted changes

Change Tracking Example

// Check for changes
if (customerUoW.IsDirty)
{
    var added = customerUoW.GetAddedEntities();
    var modified = customerUoW.GetModifiedEntities();
    var deleted = customerUoW.GetDeletedEntities();
    
    Console.WriteLine($"Changes: Added={added.Count()}, Modified={modified.Count()}, Deleted={deleted.Count()}");
    
    // Detailed tracking
    foreach (var addedIndex in added)
    {
        var entity = customerUoW.Units[addedIndex];
        Console.WriteLine($"Added: {entity.Name}");
    }
}

Events

The UnitOfWork provides comprehensive event support for lifecycle management:

Event When Fired Cancellable
PreCreate Before creating new entity ✅ Yes
PostCreate After creating new entity ✅ Yes
PreInsert Before inserting to data source ✅ Yes
PostInsert After inserting to data source ❌ No
PreUpdate Before updating in data source ✅ Yes
PostUpdate After updating in data source ❌ No
PreDelete Before deleting from data source ✅ Yes
PostDelete After deleting from data source ❌ No
PreCommit Before committing changes ✅ Yes
PostCommit After committing changes ❌ No
PostEdit After property changes ❌ No

Event Handling Example

// Subscribe to validation events
customerUoW.PreInsert += (sender, args) => 
{
    var customer = (Customer)sender;
    if (string.IsNullOrEmpty(customer.Name))
    {
        args.Cancel = true;
        args.ErrorMessage = "Customer name is required";
    }
};

// Track property changes
customerUoW.PostEdit += (sender, args) => 
{
    Console.WriteLine($"Property {args.PropertyName} changed to {args.PropertyValue}");
    
    // Log to audit trail
    auditService.LogChange(customerUoW.CurrentItem.CustomerID, args.PropertyName, args.PropertyValue);
};

// Final validation before commit
customerUoW.PreCommit += (sender, args) => 
{
    if (!ValidateAllCustomers())
    {
        args.Cancel = true;
        args.ErrorMessage = "Validation failed - please fix errors before saving";
    }
};

// Success notification
customerUoW.PostCommit += (sender, args) => 
{
    MessageBox.Show("Changes saved successfully!");
    RefreshUI();
};

Undo/Redo Functionality

Undo Methods

public void UndoLastChange()    // Undoes the last change made to the collection
public void UndoDelete()         // Undoes the most recent delete operation

Undo Example

// Make a change
var customer = customerUoW.CurrentItem;
var originalName = customer.Name;
customer.Name = "New Name";

Console.WriteLine($"Changed name from '{originalName}' to '{customer.Name}'");

// Undo the change
customerUoW.UndoLastChange();
Console.WriteLine($"Name restored to: '{customer.Name}'");

// Delete and undo example
var customerToDelete = customerUoW.CurrentItem;
customerUoW.Delete(customerToDelete);
Console.WriteLine($"Customer {customerToDelete.Name} deleted");

// Restore deleted customer
customerUoW.UndoDelete();
Console.WriteLine($"Customer {customerToDelete.Name} restored");
💡 Tip:

Undo functionality is limited to the current session. Once you commit changes to the database, use the Rollback method to revert uncommitted changes.

Logging

Logging Properties & Methods

public bool IsLogging { get; set; }           // Enable/disable change logging
public bool SaveLog(string pathandname)       // Save change log to JSON file

Logging Example

// Enable logging
customerUoW.IsLogging = true;

// Make changes
customerUoW.New();
var customer = customerUoW.CurrentItem;
customer.Name = "John Doe";
customer.Email = "john@example.com";

customerUoW.CurrentItem.Name = "John Smith"; // This change will be logged

// Save log to file
var logPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 
                          "MyApp", "Logs", $"customer_changes_{DateTime.Now:yyyyMMdd}.json");

Directory.CreateDirectory(Path.GetDirectoryName(logPath));

var success = customerUoW.SaveLog(logPath);
if (success)
{
    Console.WriteLine($"Change log saved to: {logPath}");
}
📝 Log Format:

The log is saved as JSON and includes timestamps, entity information, property changes, and operation types. This is useful for auditing and debugging purposes.

Capability Interfaces (Phase 3)

Since UnitofWork<T> implements eight marker interfaces you can detect capabilities at runtime without casting to the concrete type:

InterfaceCapabilityKey Members
IRevertablePer-item server revertRevertItem, RevertItemAsync
IBatchCommittableChunked batch commitCommitBatchAsync
IExportableData exportToDataTable, ToJsonAsync, ToCsvAsync
IImportableData importLoadFromJsonAsync, LoadFromCsvAsync
IAggregatableAggregate calculationsSum, Average, Count
IUndoableUndo/Redo stackUndoLastAction, RedoLastAction, EnableUndo
IMergeableServer-merge refreshRefreshAsync
IUnitofWorkHistoryQuery history & change auditGetChangeSummary, GetQueryHistory, CloneItem

Runtime Capability Detection

IUnitofWork uow = GetUnitOfWork(blockName); // returns non-generic IUnitofWork

// Detect and use capabilities safely
if (uow is IBatchCommittable batchable)
{
    var result = await batchable.CommitBatchAsync(batchSize: 50);
    Console.WriteLine($"Committed {result.TotalCommitted} records.");
}

if (uow is IExportable exportable)
{
    using var stream = File.Create("export.json");
    await exportable.ToJsonAsync(stream);
}

if (uow is IUnitofWorkHistory hist)
{
    var summary = hist.GetChangeSummary();
    Console.WriteLine($"Inserted: {summary.InsertedCount}, Updated: {summary.UpdatedCount}, Deleted: {summary.DeletedCount}");
}

Batch Commit (IBatchCommittable)

Commit large sets of changes in configurable chunks, with progress reporting and cancellation support. Implements IBatchCommittable.

Signature

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

CommitBatchResult Properties

public class CommitBatchResult
{
    public bool Success { get; set; }         // Overall success flag
    public int TotalCommitted { get; set; }   // Records persisted
    public List<string> Errors { get; set; } // Per-batch error messages
}

Example — Import 10,000 Records in Batches of 200

// Add a large number of entities
for (int i = 0; i < 10_000; i++)
    uow.Add(new Product { Name = $"Product {i}", Price = i * 1.5m });

var progress = new Progress<CommitBatchProgress>(p =>
    Console.WriteLine($"Batch {p.BatchNumber}/{p.TotalBatches} — {p.Committed} committed"));

var result = await uow.CommitBatchAsync(batchSize: 200, progress, CancellationToken.None);

if (result.Success)
    Console.WriteLine($"Done — {result.TotalCommitted} records saved.");
else
    foreach (var err in result.Errors)
        Console.Error.WriteLine(err);
💡 When to use CommitBatchAsync vs Commit

Use CommitBatchAsync when inserting or updating thousands of records to avoid database time-outs and to give UI progress feedback. Use Commit() for normal form-save operations with a small number of changes.

OBL Change Inspection (Phase 1)

The underlying ObservableBindingList<T> exposes rich field-level change inspection methods added in Phase 1:

MethodReturnsDescription
GetChangedFields(item)IReadOnlyList<string>Field names modified since the item was loaded
GetFieldDelta(item, field)FieldDeltaOriginal and current value pair for a field
HasFieldChanges(item)boolWhether the item has any field-level changes
GetInserted()IReadOnlyList<T>All newly inserted items not yet committed
GetUpdated()IReadOnlyList<T>All modified items
GetDeleted()IReadOnlyList<T>Soft-deleted items still in the list
GetDirty()IReadOnlyList<T>All items in a non-Unchanged state
GetChangeSetSummary()ChangeSetSummaryCounts of inserted / updated / deleted items
GetOriginalValue(item, field)objectSnapshot value captured at first modification

Example — Per-Item Delta Report

await customerUoW.Get();

var customer = customerUoW.Units.First();
customer.Name   = "Updated Name";
customer.Status = "Premium";

// Field-level inspection
var changedFields = customerUoW.Units.GetChangedFields(customer);
foreach (var field in changedFields)
{
    var delta = customerUoW.Units.GetFieldDelta(customer, field);
    Console.WriteLine($"{field}: '{delta.OriginalValue}' → '{delta.CurrentValue}'");
}

// Summary
var summary = customerUoW.Units.GetChangeSetSummary();
Console.WriteLine($"Inserted={summary.InsertedCount}, Updated={summary.UpdatedCount}, Deleted={summary.DeletedCount}");

Revert & Merge (IRevertable / IMergeable)

IRevertable — Discard Changes for a Single Item

// Strongly typed (compile-time)
bool ok = customerUoW.RevertItem(customer);
await customerUoW.RevertItemAsync(customer);

// Via non-generic IUnitofWork (runtime cast)
if (uow is IRevertable r)
    r.RevertItem(customer); // object overload

IMergeable — Re-fetch from Server & Resolve Conflicts

// Re-fetch and merge with conflict resolution
var result = await customerUoW.RefreshAsync(
    filters: null,
    conflictMode: ConflictMode.ServerWins,  // or ClientWins / KeepBoth / ThrowOnConflict
    ct: CancellationToken.None);

if (result.Flag == Errors.Ok)
    Console.WriteLine("Data refreshed — server values applied to conflicting records.");
ConflictModeBehaviour
ServerWinsServer value replaces local change
ClientWinsLocal change is kept
KeepBothBoth rows retained (new server row appended)
ThrowOnConflictThrows MergeConflictException on first conflict

Change History & Audit (IUnitofWorkHistory)

MemberReturnsDescription
GetChangeSummary()ChangeSummaryCounts of inserted / updated / deleted items in current session
GetQueryHistory()IReadOnlyList<QueryHistoryEntry>Log of all Get / GetQuery calls with timestamps and filter snapshots
ClearQueryHistory()voidResets the query history log
CloneItem(item, deep)TDeep-copy or shallow-copy of an entity (deep uses binary / JSON serialization)
GetChangeLog()List<ChangeRecord>Full per-property change audit log (requires IsLogging = true)

Example — Audit Dashboard

// Summary stats
var summary = customerUoW.GetChangeSummary();
Console.WriteLine($"Session changes: +{summary.InsertedCount} / ~{summary.UpdatedCount} / -{summary.DeletedCount}");

// Query history
foreach (var entry in customerUoW.GetQueryHistory())
    Console.WriteLine($"[{entry.ExecutedAt:HH:mm:ss}] {entry.QueryText}");

// Clone before editing (non-destructive preview)
var original = customerUoW.CloneItem(customerUoW.CurrentItem, deep: true);
customerUoW.CurrentItem.Status = "Suspended";
// cancel? restore original
// customerUoW.Units[idx] = original;

Export & Import (IExportable / IImportable)

Export API

DataTable dt  = customerUoW.ToDataTable();

using var js  = File.Create("customers.json");
await customerUoW.ToJsonAsync(js);

using var csv = File.Create("customers.csv");
await customerUoW.ToCsvAsync(csv, delimiter: ',');

Import API

// Import from JSON
using var js = File.OpenRead("customers.json");
int imported = await customerUoW.LoadFromJsonAsync(js, clearFirst: true);
Console.WriteLine($"{imported} records imported from JSON");

// Import from CSV
using var csv = File.OpenRead("customers.csv");
imported = await customerUoW.LoadFromCsvAsync(
    csv, delimiter: ',', clearFirst: false, hasHeaderRow: true);
Console.WriteLine($"{imported} records appended from CSV");
📋 Note

Import methods populate the in-memory Units collection and mark all loaded items as Added. Call Commit() to persist them to the database, or keep in list mode as an in-memory dataset.

Advanced Aggregates (IAggregatable)

MethodReturnsDescription
Sum(field)decimalSum of all values for a numeric field
SumWhere(field, predicate)decimalConditional sum
Average(field)decimalAverage of all values
AverageWhere(field, predicate)decimalConditional average
Min(field)objectMinimum value (boxed)
Max(field)objectMaximum value (boxed)
CountWhere(predicate)intCount matching a predicate
GroupBy(field)Dictionary<object, List<T>>In-memory grouping by field value
DistinctValues(field)List<object>Unique values for a field

Example — Order Summary Statistics

await orderUoW.Get();

decimal totalRevenue    = orderUoW.Sum("Total");
decimal avgOrder        = orderUoW.Average("Total");
decimal bigOrderRevenue = orderUoW.SumWhere("Total", o => o.Total > 500m);
int     bigOrderCount   = orderUoW.CountWhere(o => o.Total > 500m);
var     byCountry       = orderUoW.GroupBy("ShipCountry");

Console.WriteLine($"Revenue: {totalRevenue:C}  Avg: {avgOrder:C}  Big orders: {bigOrderCount}");
foreach (var (country, orders) in byCountry)
    Console.WriteLine($"  {country}: {orders.Count} orders");

Best Practices

🔒 Resource Management

Always use using statements since UnitOfWork implements IDisposable

📡 Event Handling

Use lifecycle events for validation and business logic implementation

💾 Change Detection

Always check IsDirty before closing forms or navigating away

⚡ Async Operations

Use async methods for all database operations to prevent UI blocking

📊 Audit Trails

Enable logging for audit trails and debugging purposes

✅ Validation

Use PreCommit event for final validation before persisting changes

⚠️ Important Notes:
  • When using identity columns (IsIdentity = true), the primary key value will be automatically updated after successful insert operations
  • Always handle exceptions when working with database operations
  • Use filters efficiently - apply them at the database level when possible
  • In list mode, changes are only in memory until explicitly persisted