UnitOfWork<T>
Generic Unit of Work pattern implementation with change tracking and validation
UnitofWork.Core.cs— Constructors (7), ~30 properties, OBL wiring, helper composition, ChangeSummary, Export/Import, DisposeUnitofWork.CRUD.cs— Read/Get/New/Add/Update/Insert/Delete with async variantsUnitofWork.Core.Extensions.cs— 13 lifecycle events (Pre/Post Create,Insert,Update,Delete,Query,Commit,Edit), Commit/Rollback, AddRange/UpdateRange/DeleteRange, RevertItem, FindAsyncUnitofWork.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 LoadingIUnitofWorkCapabilities.cs— 7 interfaces: IRevertable, IBatchCommittable, IExportable, IImportable, IAggregatable, IUndoable, IMergeable, IUnitofWorkHistoryUnitOfWorkFactory.cs(517 lines) — Runtime type creation, caching, validationUnitOfWorkWrapper.cs(1164 lines) — Late-bound wrapper with full CRUD, navigation, eventsUnitOfWorkWrapperExtensions.cs— ForEachRecordAsync, FindRecord, BatchInsertAsync, etc.Helpers/— StateHelper, DataHelper, CollectionHelper, ValidationHelper, EventHelper, DefaultsHelper, QueryHistory, ExportHelperModels/— QueryHistoryEntry, ChangeSummary, CommitBatchProgress, CommitBatchResult
📚 Table of Contents
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.
- 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");
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}");
}
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:
| Interface | Capability | Key Members |
|---|---|---|
IRevertable | Per-item server revert | RevertItem, RevertItemAsync |
IBatchCommittable | Chunked batch commit | CommitBatchAsync |
IExportable | Data export | ToDataTable, ToJsonAsync, ToCsvAsync |
IImportable | Data import | LoadFromJsonAsync, LoadFromCsvAsync |
IAggregatable | Aggregate calculations | Sum, Average, Count |
IUndoable | Undo/Redo stack | UndoLastAction, RedoLastAction, EnableUndo |
IMergeable | Server-merge refresh | RefreshAsync |
IUnitofWorkHistory | Query history & change audit | GetChangeSummary, 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);
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:
| Method | Returns | Description |
|---|---|---|
GetChangedFields(item) | IReadOnlyList<string> | Field names modified since the item was loaded |
GetFieldDelta(item, field) | FieldDelta | Original and current value pair for a field |
HasFieldChanges(item) | bool | Whether 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() | ChangeSetSummary | Counts of inserted / updated / deleted items |
GetOriginalValue(item, field) | object | Snapshot 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.");
| ConflictMode | Behaviour |
|---|---|
ServerWins | Server value replaces local change |
ClientWins | Local change is kept |
KeepBoth | Both rows retained (new server row appended) |
ThrowOnConflict | Throws MergeConflictException on first conflict |
Change History & Audit (IUnitofWorkHistory)
| Member | Returns | Description |
|---|---|---|
GetChangeSummary() | ChangeSummary | Counts of inserted / updated / deleted items in current session |
GetQueryHistory() | IReadOnlyList<QueryHistoryEntry> | Log of all Get / GetQuery calls with timestamps and filter snapshots |
ClearQueryHistory() | void | Resets the query history log |
CloneItem(item, deep) | T | Deep-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");
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)
| Method | Returns | Description |
|---|---|---|
Sum(field) | decimal | Sum of all values for a numeric field |
SumWhere(field, predicate) | decimal | Conditional sum |
Average(field) | decimal | Average of all values |
AverageWhere(field, predicate) | decimal | Conditional average |
Min(field) | object | Minimum value (boxed) |
Max(field) | object | Maximum value (boxed) |
CountWhere(predicate) | int | Count 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
- 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